﻿using System;
using System.Text.RegularExpressions;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Collections.Generic;

using Rcon.Packets;

namespace Rcon
{
	public class RConV1
	{
		#region Event Handling

		public EventHandler<PacketReceivedEventArgs> PacketReceived;

		protected bool CanFireEvent
		{
			get { return PacketReceived != null; }
		}

		protected void FireEvent(object sender, PacketReceivedEventArgs e)
		{
			PacketReceived(sender, e);
		}

		#endregion

		private List<string> knownCommands = new List<string>();

		private bool connected = false;
		private string password = null;

		private IPEndPoint hostEndpoint = null;
		private UdpClient uc = new UdpClient();

        private List<Match> matches = new List<Match>();        // Map Rotation Lists
        
        private List<Message> messages = new List<Message>();   // Message Rotation Lists

        private ServerState serverState = new ServerState();    // Server State

        // Rotation Counters
        private int rotationCounter = 0;
        private int messageCounter = 0;

        // Map and Gametype Checkers
        private MapLookup mapChecker = new MapLookup();
        private GametypeLookup gameChecker = new GametypeLookup();

		public RConV1()
		{
			knownCommands.Add("dvardump");
			knownCommands.Add("dvarlist");
			knownCommands.Add("playlist_excludeMap");
			knownCommands.Add("playlist_excludeGametypeMap");
			knownCommands.Add("playlist_excludeGametype");
			knownCommands.Add("teamstatus");
			knownCommands.Add("g_gametype");
            knownCommands.Add("playlist");
			knownCommands.Add("mapname");
			knownCommands.Add("sv_mapRotation");
			knownCommands.Add("say");

			PacketReceived += new EventHandler<PacketReceivedEventArgs>(this_OnPacketReceived);
		}

        #region Connect
        /// <summary>
		/// Starts this.Listen as well as checks for a connection to the server.
		/// Blocks the thread until the connection is confiremd.
		/// </summary>
		/// <param name="remoteIP"></param>
		/// <param name="remotePort"></param>
		public bool Connect(string remoteIP, int remotePort, string password)
		{
			if (connected)
			    return true;

			hostEndpoint = new IPEndPoint(IPAddress.Parse(remoteIP), remotePort);
			uc.Connect(hostEndpoint);
			this.Listen();

			Packet connectionPacket = new CommandPacket(password, "dvardump", null);

			// manual thread suspend until the connection is good setup
			TimeSpan timeout = TimeSpan.FromSeconds(10);
			DateTime connectionStart = DateTime.Now;
			TimeSpan elapsedTime = TimeSpan.FromSeconds(0);

			// send the packet
			this.Send(connectionPacket);

			while (!connected && elapsedTime < timeout)
			{
				System.Threading.Thread.Sleep(500);
				elapsedTime = DateTime.Now - connectionStart;
			}
            // If connected == true, enable tab control to do stuff in GUI
            if (connected)
                this.password = password;
            else
                this.password = null;
			return connected;
		}

        public bool Connected
        {
            get { return connected; }
        }
        #endregion

        /// <summary>
		/// 
		/// </summary>
		/// <param name="packet"></param>
		public void Send(Packet packet)
		{
            try
            {
                uc.Send(packet.Bytes, packet.Bytes.Length);
            }
            catch { }
		}

		/// <summary>
		/// Stops listening and resets all the connection details.
		/// </summary>
		public void Reset()
		{
			try
			{
				connected = false;
				password = null;

				hostEndpoint = null;

				uc.Close();
				uc = new UdpClient();
			}
			catch
			{
				uc.Close();
				uc = new UdpClient();
			}
		}

        #region Receiving
        /// <summary>
		/// Starts listening for responses from the server.
		/// </summary>
		public void Listen()
		{
			uc.BeginReceive(new AsyncCallback(this.uc_Receive), null);

			while (!uc.Client.IsBound)
			{
				System.Threading.Thread.Sleep(500);
			}
		}

		private void uc_Receive(IAsyncResult result)
		{
			try
			{
				if (result.IsCompleted)
				{
					// do some work
					IPEndPoint respondingEndpoint = null;
					byte[] bytes = uc.EndReceive(result, ref respondingEndpoint);

					// a little security
					if (!respondingEndpoint.Address.Equals(hostEndpoint.Address))
					{
						// NOT OUR HOST!!! BAIL OUT, DISCONNECT
						this.Reset();
						return;
					}

					uc.BeginReceive(new AsyncCallback(this.uc_Receive), null);	// start listening again as soon as things are clear

					// scan the first 5 bytes and make sure they are what we expect from the server response FF FF FF FF 01
					bool bailOnProcessing = false;
					for (int i = 0; i < 5; i++)
					{
						if (i < 4 && bytes[i] != 0xFF)
						{
							bailOnProcessing = true;
							break;
						}
						else if (i == 4 && bytes[i] != 0x01)
						{
							bailOnProcessing = true;
							break;
						}
					}

					// got a bad packet, but not malicious
					if (bailOnProcessing)
					{
						return;
					}

					ResponsePacket packet = new ResponsePacket(bytes);
					PacketReceivedEventArgs e = new PacketReceivedEventArgs();
					e.Packet = packet;

					if (this.CanFireEvent)
					{
						this.FireEvent(this, e);
					}
				}
				else
				{
					this.Reset();	// some sort of connection error
				}
			}
			catch (ObjectDisposedException)
			{
				// eat this, stupid .NET framework has planned exceptions for
				// closing sockets
			}
		}

		/// <summary>
		/// Sender will always be "this' internally. Extending classes may change that.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="?"></param>
		private void this_OnPacketReceived(object sender, PacketReceivedEventArgs e)
		{
			if (!e.Packet.Validate())
			{
				return;
			}

			if (!connected)
			{
				connected = true;
				//return;
			}

            string curMap = e.Packet.GetMapname();
            if (curMap != null && this.mapChecker.IsValidName(curMap))
            {
                this.serverState.CurrentMap = new Map(curMap, this.mapChecker.GetPrettyName(curMap));
                if(this.serverState.PreviousMap == null || this.serverState.PreviousMap.MapName == null ||  !this.serverState.PreviousMap.MapName.Equals(curMap))
                {
                    this.serverState.PreviousMap = new Map(curMap, this.mapChecker.GetPrettyName(curMap));
                    setNewGametypeMap();
                }
            }
            int newPlaylist = e.Packet.GetPlaylist();
            if (!newPlaylist.Equals(-1) && this.gameChecker.IsValidPlayList(newPlaylist.ToString()))
            {
                if(this.serverState.GameType == null || this.serverState.GameType.GameName == null || !this.serverState.GameType.GameName.Equals(this.gameChecker.GetPlayListName(newPlaylist)))
                    this.serverState.GameType = new GameType(newPlaylist, this.gameChecker.GetPlayListName(newPlaylist));
            }
		}
        #endregion

        #region Map Rotation I/O
        public bool saveMapRotationConfig(List<Match> config, string fileName)
        {
            string sequence = string.Empty;
            foreach (Match entry in config)
                sequence += entry.GameType.PlayListNum + " " + entry.Map.MapName + ";";
            try
            {
                File.WriteAllText(fileName, sequence);
                return true;
            }
            catch
            {
                return false;
            }
        }

        public List<Match> loadMapRotationConfig(string fileName)
        {
            string text = File.ReadAllText(fileName);
            string[] options = Regex.Split(text, ";");
            string[] args;
            int playlistNum;
            List<Match> listBoxList = new List<Match>();
            foreach (string pmp in options)
            {
                if (pmp.Equals(string.Empty))
                    continue;
                args = Regex.Split(pmp, " ");
                if(this.gameChecker.IsValidPlayList(args[0]) && this.mapChecker.IsValidName(args[1]))               // Something weird happened with gametype map pair, skip if invalid
                {
                    playlistNum = Convert.ToInt16(args[0]);
                    listBoxList.Add(new Match(new Map(args[1], this.mapChecker.GetPrettyName(args[1])), new GameType(playlistNum, this.gameChecker.GetPlayListName(playlistNum))));
                }
            }
            return listBoxList;
        }
        #endregion

        #region Message Rotation I/O
        public bool saveMessageRotationConfig(List<Message> config, string fileName)
        {
            string sequence = string.Empty;
            foreach (Message entry in config)
            {
                sequence += entry.ToString() + Environment.NewLine;
            }
            try
            {
                File.WriteAllText(fileName, sequence);
                return true;
            }
            catch
            {
                return false;
            }
        }

        public List<Message> loadMessageRotationConfig(string fileName)
        {
            string text = File.ReadAllText(fileName);
            string[] options = Regex.Split(text, Environment.NewLine);
            string[] args;
            int dur;
            List<Message> listBoxList = new List<Message>();
            foreach (string entry in options)
            {
                if (entry.Equals(string.Empty))
                    continue;
                args = Regex.Split(entry, ";");
                if (args.Length.Equals(4))
                {
                    if (!string.IsNullOrEmpty(args[0]) && int.TryParse(args[1], out dur) && dur > 0 && this.gameChecker.IsValidPlayList(args[2]) && this.mapChecker.IsValidName(args[3]))
                        listBoxList.Add(new Message(args[0], dur, args[2], args[3]));
                }
            }
            return listBoxList;
        }
        #endregion

        #region Set Match & Message Lists
        public void setGametypeMapLists(List<Match> matches)
        {
            this.serverState.PreviousMap = new Map();
            this.rotationCounter = 0;
            this.matches = matches;
        }

        public void setMessageLists(List<Message> messages)
        {
            this.messages = messages;
            this.messageCounter = 0;
        }
        #endregion

        #region Queries
        public void mapQuery()
        {
            CommandPacket packet = new CommandPacket(this.password, "mapname", null);
            this.Send(packet);
        }

        public void playListQuery()
        {
            CommandPacket packet = new CommandPacket(this.password, "playlist", null);
            this.Send(packet);
        }
        #endregion

        #region Server Mutators
        private void setNewGametypeMap()
        {
            // Are there matches listed to rotate on?
            if (this.matches.Count > 0)
            {
                // Check to make sure counter isn't larger than maps and gametypes list (check both for consistency)
                if (this.rotationCounter >= this.matches.Count)
                    this.rotationCounter = 0;
                string[] args = new string[1];                                  // Input arguments for command packet

                // Playlist Set Packet
                args[0] = this.matches[this.rotationCounter].GameType.PlayListNum.ToString();
                CommandPacket packet = new CommandPacket(this.password, "playlist", args);
                // Send the packet 3 times to make sure it gets there
                this.Send(packet);
                System.Threading.Thread.Sleep(4000);    // Wait 4 seconds and retransmit
                this.Send(packet);
                System.Threading.Thread.Sleep(4000);    // Wait 4 seconds and retransmit
                this.Send(packet);

                // Exclude Map Packet
                string desiredMap = this.matches[this.rotationCounter].Map.MapName;
                string omitList = string.Empty;
                foreach (string map in mapChecker.GetUglyNames())
                {
                    if (map.Equals(desiredMap) == false)
                        omitList += map + " ";
                }
                args[0] = " ";
                packet = new CommandPacket(this.password, "playlist_excludeMap", args);
                // Send the packet 3 times to make sure it gets there
                this.Send(packet);
                System.Threading.Thread.Sleep(4000);    // Wait 4 seconds and retransmit
                this.Send(packet);
                System.Threading.Thread.Sleep(4000);    // Wait 4 seconds and retransmit
                this.Send(packet);
                args[0] = omitList;
                packet = new CommandPacket(this.password, "playlist_excludeMap", args);
                // Send the packet 3 times to make sure it gets there
                this.Send(packet);
                System.Threading.Thread.Sleep(4000);    // Wait 4 seconds and retransmit
                this.Send(packet);
                System.Threading.Thread.Sleep(4000);    // Wait 4 seconds and retransmit
                this.Send(packet);
                this.rotationCounter++;
            }
        }
        
        public int displayMessage()
        {
            int newElapse = 10;
            // Check to make sure we have the proper server state information before we get too deep in this
            if (this.serverState.GameType == null || this.serverState.GameType.PlayListNum.Equals(-1) || this.serverState.CurrentMap == null || this.serverState.CurrentMap.MapName == null)
                return newElapse;
            // Are there messages to rotate on?
            if (this.messages.Count > 0)
            {
                if (this.messageCounter >= this.messages.Count)                 // Check if counter is larger than message size
                    this.messageCounter = 0;
                // Get current gametype and see if we have a message that is appropriate for it
                bool display = false;
                List<int> gameList = new List<int>();
                for (int index = this.messageCounter; index < this.messages.Count; index++ )              // Each List<int> in visiblePlaylists can have multiple playlist numbers, i.e.: Team Deathmatch has playlist numbers 1, 9, and 17
                {
                    gameList = this.messages[index].GetVisiblePlayList;
                    if ((gameList.Contains(this.serverState.GameType.PlayListNum) || gameList.Contains(0)) && (this.messages[index].GetMap.Equals("Any") || this.messages[index].GetMap.Equals(this.serverState.CurrentMap.PrettyName)))
                    {
                        display = true;
                        this.messageCounter = index;
                        break;
                    }
                }
                if (!display)
                {
                    for (int index = 0; index < this.messageCounter; index++)
                    {
                        gameList = this.messages[index].GetVisiblePlayList;
                        if ((gameList.Contains(this.serverState.GameType.PlayListNum) || gameList.Contains(0)) && (this.messages[index].GetMap.Equals("Any") || this.messages[index].GetMap.Equals(this.serverState.CurrentMap.PrettyName)))
                        {
                            display = true;
                            this.messageCounter = index;
                            break;
                        }
                    }
                }
                if (display)
                {
                    string[] args = new string[1];                                  // Input arguments for command packet
                    // Message Packet
                    args[0] = this.messages[this.messageCounter].GetMessage;
                    newElapse = this.messages[this.messageCounter].GetDuration;
                    this.messageCounter++;
                    CommandPacket packet = new CommandPacket(this.password, "say", args);
                    this.Send(packet);
                }
            }
            return newElapse;
        }
        #endregion
    }
}